use anyhow::{anyhow, Result};
use chrono::{DateTime, SecondsFormat, Utc};
use mongodb::bson::{
    document::{ValueAccessError, ValueAccessResult},
    oid::ObjectId,
    Bson, Document,
};
use std::convert::TryFrom;
use tide::{Error, Request, StatusCode};

#[inline]
pub fn format_datetime(time: &DateTime<Utc>) -> String {
    time.to_rfc3339_opts(SecondsFormat::Millis, true)
}

pub fn make_json_friendly(document: &mut Document) -> ValueAccessResult<()> {
    let time_fields = document
        .keys()
        .filter_map(|key: &String| {
            document
                .get_datetime(key)
                .map(|time| (key.clone(), format_datetime(time)))
                .ok()
        })
        .collect::<Vec<_>>();
    for (key, time) in time_fields {
        if let Some(value) = document.get_mut(&key) {
            *value = Bson::String(time)
        }
    }
    if let Ok(id) = document.get_object_id("_id") {
        let object_id = id.to_hex();
        document.remove("_id");
        document.insert("objectId", object_id);
    }
    Ok(())
}

fn convert_object_id(value: &Bson) -> Result<Bson> {
    use core::iter::FromIterator;
    let new_value = match value {
        Bson::String(id) => Bson::ObjectId(ObjectId::with_string(&id)?),
        Bson::Array(array) => array
            .into_iter()
            .flat_map(|id| ObjectId::with_string(id.as_str().unwrap()))
            .collect(),
        Bson::Document(doc) => Bson::Document(Document::from_iter(
            doc.iter()
                .map(|(key, val)| (key.to_string(), convert_object_id(val).unwrap())),
        )),
        _ => return Err(anyhow!("Invalid objectId value")),
    };
    Ok(new_value)
}

pub fn fix_query_filter(document: &mut Document) -> Result<()> {
    if let Some(value) = document.get("objectId") {
        let new_value = convert_object_id(value)?;
        document.remove("objectId");
        document.insert("_id", new_value);
    }
    convert_date_field(document);
    Ok(())
}

pub async fn parse_request_body<State>(req: &mut Request<State>) -> tide::Result<Document> {
    let value: serde_json::Value = req
        .body_json()
        .await
        .map_err(|_| Error::from_str(StatusCode::BadRequest, "Invalid json"))?;
    if let Ok(Bson::Document(document)) = Bson::try_from(value) {
        Ok(document)
    } else {
        Err(Error::from_str(
            StatusCode::BadRequest,
            "Expect json object in request body",
        ))
    }
}

pub fn convert_date_field(document: &mut Document) {
    for key in &document.keys().map(|key| key.clone()).collect::<Vec<_>>() {
        if let Some(value) = document.get_mut(key) {
            if let Some(doc) = value.as_document_mut() {
                if let Ok(tp) = doc.get_str("__type") {
                    if tp == "Date" {
                        let date = DateTime::parse_from_rfc3339(doc.get_str("iso").unwrap()).unwrap();
                        *value = Bson::DateTime(date.into());
                    }
                } else {
                    convert_date_field(doc);
                }
            } else if let Some(array) = value.as_array_mut() {
                for item in array {
                    if let Some(doc) = item.as_document_mut() {
                        if let Ok(tp) = doc.get_str("__type") {
                            if tp == "Date" {
                                let date = DateTime::parse_from_rfc3339(doc.get_str("iso").unwrap()).unwrap();
                                *item = Bson::DateTime(date.into());
                            }
                        } else {
                            convert_date_field(doc);
                        }
                    }
                }
            }
        }
    }
}

pub fn get_integer(doc: &Document, key: &str) -> ValueAccessResult<i64> {
    if let Some(value) = doc.get(key) {
        if let Some(num) = value.as_i64() {
            Ok(num)
        } else if let Some(num) = value.as_i32() {
            Ok(num as i64)
        } else if let Some(num) = value.as_f64() {
            Ok(num as i64)
        } else {
            Err(ValueAccessError::UnexpectedType)
        }
    } else {
        Err(ValueAccessError::NotPresent)
    }
}

pub fn get_number(doc: &Document, key: &str) -> ValueAccessResult<f64> {
    if let Some(value) = doc.get(key) {
        if let Some(num) = value.as_f64() {
            Ok(num)
        } else if let Some(num) = value.as_i64() {
            Ok(num as f64)
        } else if let Some(num) = value.as_i32() {
            Ok(num as f64)
        } else {
            Err(ValueAccessError::UnexpectedType)
        }
    } else {
        Err(ValueAccessError::NotPresent)
    }
}
